package bittorrent

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"reflect"
	"strings"
	"time"

	"github.com/anacrolix/torrent"
	"github.com/anacrolix/torrent/metainfo"

	"downloadmanager/internal/downloader"
)

// Options for BitTorrent client behavior.
type Options struct{
    NoUpload   bool
    ListenPort int
    EnableDHT  bool
    EnableUPnP bool
    DisablePEX bool
    DisableTrackers bool
    MaxPeersGlobal     int
    MaxPeersPerTorrent int
}

type Downloader struct{ opt Options }

func New(opt Options) *Downloader { return &Downloader{opt: opt} }

// Download accepts a magnet URI or a .torrent (local path or http[s] url).
// saveDir should be a directory. The torrent will be stored under saveDir/<torrent-name>/...
// selectedFiles: optional file indices to download (based on order in torrent). If empty, downloads all files.
func (d *Downloader) Download(ctx context.Context, source string, saveDir string, selectedFiles []int, progress func(downloader.Progress)) error {
	if strings.TrimSpace(source) == "" { return errors.New("empty source") }
	if err := os.MkdirAll(saveDir, 0o755); err != nil { return err }

	cfg := torrent.NewDefaultClientConfig()
	cfg.NoUpload = d.opt.NoUpload
	cfg.DataDir = saveDir
	if d.opt.ListenPort > 0 { cfg.ListenPort = d.opt.ListenPort }
	// trackers / PEX / DHT / UPnP
	cfg.DisablePEX = d.opt.DisablePEX
	cfg.ClientTrackerConfig.DisableTrackers = d.opt.DisableTrackers
	cfg.ClientDhtConfig.NoDHT = !d.opt.EnableDHT
	// If UPnP disabled, prevent default port forwarding
	cfg.NoDefaultPortForwarding = !d.opt.EnableUPnP
	client, err := torrent.NewClient(cfg)
	if err != nil { return err }
	defer client.Close()
	// best-effort: set global peers cap if supported by library version
	if d.opt.MaxPeersGlobal > 0 {
		// try method SetMaxEstablishedConns(int)
		if m := reflect.ValueOf(client).MethodByName("SetMaxEstablishedConns"); m.IsValid() {
			m.Call([]reflect.Value{ reflect.ValueOf(d.opt.MaxPeersGlobal) })
		}
	}

	var t *torrent.Torrent
	if strings.HasPrefix(strings.ToLower(source), "magnet:") {
		t, err = client.AddMagnet(source)
		if err != nil { return err }
	} else if strings.HasSuffix(strings.ToLower(source), ".torrent") {
		// support local file or http url
		var mi *metainfo.MetaInfo
		if strings.HasPrefix(strings.ToLower(source), "http://") || strings.HasPrefix(strings.ToLower(source), "https://") {
			resp, e := http.Get(source)
			if e != nil { return e }
			defer resp.Body.Close()
			if resp.StatusCode >= 400 { return fmt.Errorf("http %d", resp.StatusCode) }
			b, e := io.ReadAll(resp.Body)
			if e != nil { return e }
			mi, e = metainfo.Load(bytes.NewReader(b))
			if e != nil { return e }
		} else {
			var e error
			mi, e = metainfo.LoadFromFile(source)
			if e != nil { return e }
		}
		// Add torrent via metainfo
		t, err = client.AddTorrent(mi)
		if err != nil { return err }
	} else {
		return errors.New("unsupported torrent source")
	}

	// ensure we stop when ctx done
	done := make(chan struct{})
	go func(){
		select {
		case <-ctx.Done():
			if t != nil { t.Drop() }
			client.Close()
		case <-done:
		}
	}()

	// Wait for info to get total length and name
	<-t.GotInfo()
	// best-effort: set per-torrent peers cap if supported
	if d.opt.MaxPeersPerTorrent > 0 {
		if m := reflect.ValueOf(t).MethodByName("SetMaxEstablishedConns"); m.IsValid() {
			m.Call([]reflect.Value{ reflect.ValueOf(d.opt.MaxPeersPerTorrent) })
		}
	}
	// Select files
	if len(selectedFiles) == 0 {
		t.DownloadAll()
	} else {
		files := t.Files()
		for _, idx := range selectedFiles {
			if idx >= 0 && idx < len(files) {
				files[idx].Download()
			}
		}
	}

	total := t.Length()
	last := time.Now()
	var lastBytes int64 = 0

	tk := time.NewTicker(500 * time.Millisecond)
	defer tk.Stop()

	for {
		select {
		case <-ctx.Done():
			close(done)
			return ctx.Err()
		case <-tk.C:
			completed := t.BytesCompleted()
			elapsed := time.Since(last)
			speed := int64(0)
			if elapsed > 0 { speed = (completed - lastBytes) * int64(time.Second) / int64(elapsed) }
			peers := reflectPeersCount(t)
			if progress != nil { progress(downloader.Progress{Total: total, Completed: completed, SpeedBps: speed, Source: "bt", Peers: peers}) }
			last = time.Now(); lastBytes = completed
			if completed >= total && total > 0 {
				close(done)
				return nil
			}
		}
	}
}

// reflectPeersCount tries to obtain peers count from t.Stats() without relying on specific struct fields.
func reflectPeersCount(t *torrent.Torrent) int {
	defer func(){ _ = recover() }()
	sv := reflect.ValueOf(t)
	if !sv.IsValid() { return 0 }
	// call Stats() if present
	if m := sv.MethodByName("Stats"); m.IsValid() {
		out := m.Call(nil)
		if len(out) == 1 {
			st := out[0]
			// try st.Connected.Peers
			cv := st.FieldByName("Connected")
			if cv.IsValid() {
				pv := cv.FieldByName("Peers")
				if pv.IsValid() && pv.Kind() == reflect.Int {
					return int(pv.Int())
				}
			}
			// try st.Peers
			pv := st.FieldByName("Peers")
			if pv.IsValid() && pv.Kind() == reflect.Int {
				return int(pv.Int())
			}
		}
	}
	return 0
}
